# Copyright (c) 2020 ARM Limited
# All rights reserved
#
# The license below extends only to copyright in the software and shall
# not be construed as granting a license to any other intellectual
# property including but not limited to intellectual property relating
# to a hardware implementation of the functionality of the software
# licensed hereunder.  You may use the software subject to the license
# terms below provided that you ensure that this notice is replicated
# unmodified and in its entirety in all distributions of the software,
# modified or unmodified, in source code or in binary form.
#
# Copyright 2019 Google, Inc.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met: redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer;
# redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution;
# neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from itertools import cycle
import logging
import shlex

Import('*')

from grammar import Grammar

from gem5_scons import Transform, warning, error

import os.path

if env['CONF']['USE_ARM_FASTMODEL']:
    env.TagImplies('arm fastmodel', 'arm isa')
else:
    Return()

def extract_var(name):
    val = env['CONF'].get(name, None)
    if val is None:
        error(f'{name} is not set')
    if env['CONF']['USE_ARM_FASTMODEL']:
        print(f'{name} = {val}')
    # Make sure the value of this variable shows up as an environment variable
    # for commands scons runs.
    env['ENV'][name] = val
    return val

pvlib_home, maxcore_home, armlmd_license_file = \
    list(map(extract_var, ('PVLIB_HOME', 'MAXCORE_HOME',
                           'ARMLMD_LICENSE_FILE')))

pvlib_home = Dir(pvlib_home)
maxcore_home = Dir(maxcore_home)

pvlib_flavor = env['CONF']['PVLIB_FLAVOR']
pvlib_lib_dir = pvlib_home.Dir('lib').Dir(pvlib_flavor)

simulation_engine_name = 'libMAXCOREInitSimulationEngine.3.so'
simulation_engine_lib = env.Command(
            Dir(env['BUILDDIR']).File(simulation_engine_name),
            pvlib_lib_dir.File(simulation_engine_name),
            MakeAction("cp ${SOURCE} ${TARGET}", Transform('COPY')))

arm_singleton_registry_name = 'arm_singleton_registry.so'
arm_singleton_registry_lib = env.Command(
            Dir(env['BUILDDIR']).File(arm_singleton_registry_name),
            pvlib_lib_dir.File(arm_singleton_registry_name),
            MakeAction("cp ${SOURCE} ${TARGET}", Transform('COPY')))


def staticify(env, name):
    ''' Check for a static version of the library named 'name', and if it
        exists return a File node for it explicitly. Otherwise pass 'name'
        through for normal processing.'''

    static_name = env.subst('${LIBPREFIX}' + name + '${LIBSUFFIX}')

    for path in env.Flatten(env['LIBPATH']):
        full_name = Dir(path).File(static_name).get_abspath()
        if os.path.isfile(full_name):
            return File(full_name)
    if env['CONF']['USE_ARM_FASTMODEL']:
        warning("Failed to find FM static lib: " + name)
    return name

# Adjust the build environment to support building in Fast Models.

env.Append(CCFLAGS='-pthread')

cpppaths = (
    pvlib_home.Dir('examples/SystemCExport/Common/include'),
    pvlib_home.Dir('include'),
    pvlib_home.Dir('include/fmruntime'),
    pvlib_home.Dir('include/fmruntime/eslapi'),
    pvlib_home.Dir('Iris/include'),

    maxcore_home.Dir('AMBA-PV/include'),
)
env.Append(CPPPATH=cpppaths)

lib_paths = (
    pvlib_lib_dir,
    pvlib_home.Dir('Iris').Dir(pvlib_flavor),
)
env.Append(LIBPATH=lib_paths)

# Per ARM's 11.16 release note, a platform build with simgen automatically
# copies libraries into the build target directory along with the other
# dependencies. Therefore, we only need to add each simgen result into rpath and
# no other shared librarires are required here.
fm_static_libs = (
    'components',
    'pvbus',
    'armctmodel',
    'fmruntime',
    'IrisSupport',
)
for lib in fm_static_libs:
    SourceLib(staticify(env, lib), tags=['arm fastmodel'])

SourceLib('atomic', tags=['arm fastmodel'])
SourceLib('dl', tags=['arm fastmodel'])
SourceLib('rt', tags=['arm fastmodel'])

class ProjectFileParser(Grammar):
    def __init__(self):
        self.log = logging.getLogger('fm_proj_ply')
        if GetOption('verbose'):
            self.log.setLevel(logging.DEBUG)

        self.yacc_kwargs['write_tables'] = False

        self.yacc_kwargs['debuglog'] = self.log
        self.yacc_kwargs['errorlog'] = self.log

        self.lex_kwargs['debuglog'] = self.log
        self.lex_kwargs['errorlog'] = self.log

    class Param(object):
        def __init__(self, is_object):
            self.is_object = is_object

        def __str__(self):
            return self._to_str(0)

    class StringParam(Param):
        def __init__(self, name, value):
            super().__init__(is_object=False)
            self.name = name
            self.value = value

        def _to_str(self, indent):
            indent_str = "    " * indent
            return indent_str + self.name + ' = \"' + self.value + '\"'

    class ObjectParam(Param):
        def __init__(self, type_name, name, params):
            super().__init__(is_object=True)
            self.type_name = type_name
            self.name = name
            self.params = params

        def _to_str(self, indent):
            indent_str = "    " * indent
            val = indent_str + self.type_name
            if self.name is not None:
                val += ' "' + self.name + '"'
            val += "\n" + indent_str + "{\n"
            for param in self.params:
                val += param._to_str(indent + 1) + "\n"
            val += indent_str + "}"
            return val

    #########
    # Lexer #
    #########

    tokens = (
        # identifier
        'ID',

        # string literal
        'STRLIT',

        # = { } ;
        'EQUALS',
        'LBRACE',
        'RBRACE',
        'SEMI',
    )

    t_ID = r'[A-Za-z_]\w*'

    def t_STRLIT(self, t):
        r'"([^"])*"'
        # strip off quotes
        t.value = t.value[1:-1]
        return t

    t_EQUALS = r'='
    t_LBRACE = r'\{'
    t_RBRACE = r'\}'
    t_SEMI = r';'

    def t_NEWLINE(self, t):
        r'\n+'
        t.lexer.lineno += t.value.count('\n')

    t_ignore = ' \t\x0c'

    ##########
    # Parser #
    ##########

    def p_object(self, t):
        'object : object_heading LBRACE params RBRACE'
        t[0] = self.ObjectParam(t[1][0], t[1][1], t[3])

    def p_object_heading_0(self, t):
        'object_heading : ID STRLIT'
        t[0] = (t[1], t[2])

    def p_object_heading_1(self, t):
        'object_heading : ID'
        t[0] = (t[1], None)

    def p_params_0(self, t):
        'params : '
        t[0] = []

    def p_params_1(self, t):
        'params : param params'
        t[0] = [t[1]] + t[2]

    def p_param_0(self, t):
        'param : ID EQUALS STRLIT SEMI'
        t[0] = self.StringParam(t[1], t[3])

    def p_param_1(self, t):
        'param : object'
        t[0] = t[1]


# If fast model is disabled, ARMLMD_LICENSE_COUNT will be 0 which will break
# the cycle() iterator below. The fast model components won't be built, but
# they still need to be set up successfully with valid license slots.
license_count = max(int(env['CONF']['ARMLMD_LICENSE_COUNT']), 1)
arm_licenses = list((Value(object()) for i in range(license_count)))
license_cycle = cycle(arm_licenses)

# HACK: Make sure the gic protocol headers are somewhere we can find them.
# These should start out alongside other headers fast model provides which we
# are already able to include, but unfortunately they're in the examples
# directory.
gicv3_comms_headers = (
        'gicv3_comms_base.h', 'gicv3_comms_if.h', 'gicv3_comms_sockets.h')
examples_common_dir = pvlib_home.Dir('examples/SystemCExport/Common')
gic_protocol_path = 'Protocols/GICv3Comms'
gic_protocol_dest = Dir(env['BUILDDIR']).Dir(gic_protocol_path)
gic_protocol_src = examples_common_dir.Dir(gic_protocol_path)

for header in gicv3_comms_headers:
    Command(gic_protocol_dest.File(header), gic_protocol_src.File(header),
            Copy('${TARGET}', '${SOURCE}'))
# Since we are copying the source files to a different directory, Scons's
# dependency scanner does not pick up the dependency between these files.
# Specify them manually.
env.Depends(gic_protocol_dest.File('gicv3_comms_base.h'),
            gic_protocol_dest.File('gicv3_comms_if.h'))
env.Depends(gic_protocol_dest.File('gicv3_comms_sockets.h'),
            gic_protocol_dest.File('gicv3_comms_if.h'))

common_headers = ('lisa_protocol_types.h', 'tlm_has_get_protocol_types.h')
for header in common_headers:
    header_target = gic_protocol_dest.Dir('include').File(header)
    header_src = examples_common_dir.Dir('include').File(header)
    Command(header_target, header_src, Copy('${TARGET}', '${SOURCE}'))

if not env['CONF']['SIMGEN']:
    env['CONF']['SIMGEN'] = os.path.join(
            env['CONF']['MAXCORE_HOME'], 'bin', 'simgen')

class ArmFastModelComponent(object):
    def __init__(self, project_file, *extra_deps, tags=None):
        if not tags:
            tags = ['arm fastmodel']
        self.tags = tags
        project_file = File(project_file)
        project_file_dir = project_file.Dir('.')

        parser = ProjectFileParser()
        proj = parser.parse_file(project_file.srcnode().abspath)

        # Top level component.
        tlc = None
        # Config name.
        config_name = None
        # simgen_arg
        simgen_command_line = ''

        # Scan for particular properties of the project.
        for param in proj.params:
            if not param.is_object:
                if param.name == 'TOP_LEVEL_COMPONENT':
                    tlc = param.value
                elif param.name == 'ACTIVE_CONFIG_LINUX':
                    config_name = param.value

        for param in proj.params:
            if param.name == config_name:
                for sub_param in param.params:
                    if sub_param.name == 'SIMGEN_COMMAND_LINE':
                        simgen_command_line = sub_param.value

        assert tlc is not None and config_name is not None

        simgen_dir = project_file_dir.Dir(config_name)

        def simgen_static(name):
            return simgen_dir.File(env.subst(
                        '${LIBPREFIX}%s${LIBSUFFIX}' % name))

        def simgen_shared(name):
            return simgen_dir.File(env.subst(
                        '${SHLIBPREFIX}%s${SHLIBSUFFIX}' % name))

        static_libs = [
            'scx-%s-%s' % (tlc, config_name),
            'scx',
        ]
        shared_libs = [
            '%s-%s' % (tlc, config_name),
        ]

        static_lib_nodes = list(map(simgen_static, static_libs))
        shared_lib_nodes = list(map(simgen_shared, shared_libs))
        # We need to use the static libraries as files so that the linker
        # doesn't use the shared versions of them instead. We need to use
        # the shared libraries by name so that the linker will apply RPATH
        # and be able to find them at run time. If a shared libary includes
        # a path, the dynamic linker will apparently ignore RPATH when looking
        # for it.
        lib_nodes = static_lib_nodes + shared_lib_nodes

        gen_dir = simgen_dir.Dir('gen')

        header = gen_dir.File('scx_evs_%s.h' % tlc)

        self.headers = [header]
        self.headerpaths = [gen_dir]
        self.libs = static_lib_nodes + shared_libs
        self.libpaths = [simgen_dir]
        # Simgen also puts required share library under the project folder.
        self.rpaths = [simgen_dir, project_file_dir]
        self.log = gen_dir.File('build_%s.log' % tlc)
        self.simgen_cmd = env.subst('${CONF["SIMGEN"]} -p %s '
            '--configuration %s -b --verbose off --num-build-cpus %d -I %s %s '
            '--build-dir %s >%s') % \
            (shlex.quote(project_file.srcnode().abspath),
             shlex.quote(config_name),
             GetOption('num_jobs'),
             shlex.quote(Dir('#/src').srcnode().abspath),
             simgen_command_line,
             shlex.quote(simgen_dir.abspath),
             shlex.quote(self.log.abspath))

        sources = [project_file]
        sources.extend(extra_deps)
        # The simgen-generated .lisa files may #include these gicv3 files, but
        # SCons does not detect this dependency since they are generated files.
        # Add the dependencies manually.
        sources.extend([gic_protocol_dest.File('gicv3_comms_sockets.h'),
                        gic_protocol_dest.File('gicv3_comms_base.h')])
        env.Command(lib_nodes + self.headers + [self.log], sources,
                    Action(self.simgen_builder, Transform('SIMGEN')))
        # Distribute simgen actions among ARM license slots. All actions which
        # have a given license as a "side effect" will be serialized relative
        # to each other, meaning the number of licenses being used concurrently
        # will never be larger than the number of license nodes.
        #
        # This allocation is fixed and may not be as optimal as a dynamic one,
        # but the difference is probably not significant.
        env.SideEffect(next(license_cycle), lib_nodes[0])

        # We need a copy of the simulation engine lib and arm_singleton_registry
        # (introduced in 11.16) alongside the executable.
        Depends(lib_nodes[0], simulation_engine_lib)
        Depends(lib_nodes[0], arm_singleton_registry_lib)

    def prepare_env(self, env):
        env.Append(LIBPATH=self.libpaths)
        env.AddLocalRPATH(*self.rpaths)
        env.Append(CPPPATH=self.headerpaths)
        # Put these libraries earlier in the list by setting priority.
        for lib in self.libs:
            SourceLib(lib, priority=1, tags=self.tags)

    def simgen_builder(self, target, source, env):
        cmd = self.simgen_cmd
        if not GetOption('verbose'):
            cmd = "@" + cmd
        res = env.Execute(cmd)
        # Print output when execution return non-zero or in verbose mode.
        if res or GetOption('verbose'):
            env.Execute('@cat %s' % self.log.abspath)
        return res


Export('ArmFastModelComponent')

PySource('m5', 'arm_fast_model.py', tags=['arm fastmodel'])
Source('fastmodel.cc', tags=['arm fastmodel', 'python'])

SimObject(
    'FastModel.py',
    sim_objects=['AmbaToTlmBridge64', 'AmbaFromTlmBridge64'],
    tags=['arm fastmodel']
)
Source('amba_to_tlm_bridge.cc', tags=['arm fastmodel'])
Source('amba_from_tlm_bridge.cc', tags=['arm fastmodel'])
Source('remote_gdb.cc', tags=['arm fastmodel'])
